/*
 * Copyright 2014 by État de Genève (Switzerland)
 * All rights reserved.
 *
 * Author: ELCA Informatique SA
 */
define([
    'moment',
    'services/config',
    'services/formatter',
    'services/dataservice',
    'common/noPoursuite',
    'common/constant',
    'i18n',
    'q'
], function (moment, config, formatter, ds, noPoursuite, constant, i18n, Q) {

    // reference object to get options depending for which type of data the validation is
    // for types that can be validated through the 'genericInit' and 'genericUpdate' functions only
    var _validationTypes = {
        percent: {
            regex: /^(100|(\d{1,2}(\.\d+)?)\%?)$/,
            format: 'percent',
            inputToValue: function (input) {
                return parseFloat(input) / 100;
            },
            placeholder: '%'
        },
        integer: {
            regex: /^[0-9]+$/,
            format: '_default',
            inputToValue: function (input) {
                return parseInt(input, 10);
            }
        },
        currency: {
            regex: /^-?((\d+)|(\d{1,3}('\d{3})*))(\.\d{1,2})?$/,
            format: 'currency',
            inputToValue: function (input) {
                return parseFloat(input.replace('\'', ''));
            }
        },
        string: {
            regex: /^.*$/,
            format: '_default',
            inputToValue: function (input) {
                return input;
            }
        },
        username: {
            regex: /^.*$/,
            format: '_default',
            inputToValue: function (input) {
                return input;
            },
            placeholder: i18n.t('cms:loginForm.'+constant.VALIDATE_USERNAME)
        },
        password: {
            regex: /^.*$/,
            format: '_default',
            inputToValue: function (input) {
                return input;
            },
            placeholder: i18n.t('cms:loginForm.'+constant.VALIDATE_PASSWORD)
        }
    };

    return {
        bindingsToActivate: [
            'validationReport',
            constant.VALIDATE_DATE,
            constant.VALIDATE_PERCENT,
            constant.VALIDATE_INTEGER,
            constant.VALIDATE_CURRENCY,
            constant.VALIDATE_STRING,
            constant.VALIDATE_USERNAME,
            constant.VALIDATE_PASSWORD,
            'noPoursuite',
            'keyLabel',
            'keyLabelMultiple'
        ],
        validationReport: {
            init: updateStyle,
            update: updateStyle
        },
        date: {
            init: dateInit,
            update: dateUpdate
        },
        percent: {
            init: genericInit.bind(undefined, constant.VALIDATE_PERCENT),
            update: genericUpdate.bind(undefined, constant.VALIDATE_PERCENT)
        },
        integer: {
            init: genericInit.bind(undefined, constant.VALIDATE_INTEGER),
            update: genericUpdate.bind(undefined, constant.VALIDATE_INTEGER)
        },
        currency: {
            init: genericInit.bind(undefined, constant.VALIDATE_CURRENCY),
            update: genericUpdate.bind(undefined, constant.VALIDATE_CURRENCY)
        },
        string: {
            init: genericInit.bind(undefined, constant.VALIDATE_STRING),
            update: genericUpdate.bind(undefined, constant.VALIDATE_STRING)
        },
        username: {
            init: genericInit.bind(undefined, constant.VALIDATE_USERNAME),
            update: genericUpdate.bind(undefined, constant.VALIDATE_USERNAME)
        },
        password: {
            init: genericInit.bind(undefined, constant.VALIDATE_PASSWORD),
            update: genericUpdate.bind(undefined, constant.VALIDATE_PASSWORD)
        },
        noPoursuite: {
            init: poursuiteInit,
            update: poursuiteUpdate
        },
        keyLabel: {
            // important: https://github.com/knockout/knockout/blob/master/src/binding/defaultBindings/value.js
            after: ['options'],
            init: selectInit,
            update: selectUpdate
        },
        keyLabelMultiple: {
            init: selectInit,
            update: selectUpdate
        }
    };

    /**
     * Custom binding for select that are populated with key/label objects and need validation
     */
    function selectInit(element, valueAccessor, allBindings) {

        // valud can be either the observable, or a composed object: { value: ko.observable(), required: true }
        var valueConf = valueAccessor(),
            value = null,
            required = false,
            // all options of the select
            options = ko.unwrap(allBindings().options),
            // if report validation is needed
            report = allBindings().validationReport,
            $el = $(element),
            fieldId = $el.attr('id'),
            isMultiple = !!$el.attr('multiple');

        if(isMultiple) {
            // init multiple select with selectpicker addon for bootstrap
            // http://silviomoreto.github.io/bootstrap-select/
            $el.addClass('selectpicker')
                .selectpicker({
                    noneSelectedText: '---',
                    iconBase: 'mi',
                    tickIcon: 'mi-status-4',
                    selectedTextFormat: 'count > 3',
                    countSelectedText: '{0} sur {1} sélect.'
                });
        }


        // find the real value observable (whether inside the valueConf, or is the valueConf
        if(!ko.isObservable(valueConf) && valueConf && ko.isObservable(valueConf.value)) {
            required = !!valueConf.required;
            value = valueConf.value;
        } else if(ko.isObservable(valueConf)) {
            value = valueConf;
        }

        if(value) {
            $el.change(function () {

                var elementValue;
                if(isMultiple) {
                    // returns only the options whose key is in the $el.val() array
                    // will be an array of key in this case
                    elementValue = _.filter(options, function (kv) {

                        return _.contains($el.val(), kv.keyStr);
                    });
                } else {
                    // find the key/value from the select val
                    elementValue = _.findWhere(options, { keyStr: $(this).val() });
                }

                // if there's value(s), report the select as valid and update the value(s)
                // else, validate according to required flag
                if(elementValue && (!_.isArray(elementValue) || elementValue.length > 0)) {
                    value(elementValue);
                    // setValid
                    reportValid(fieldId, report, value);
                } else {
                    if(required){
                        reportInvalid(fieldId, report, value);
                    } else {
                        reportValid(fieldId, report, value);
                    }
                    value(isMultiple ? [] : null);
                }
            });
        }
    }

    /**
     * Custom binding for select that are populated with key/label objects and need validation
     */
    function selectUpdate(element, valueAccessor, allBindings) {
        var valueConf = valueAccessor(),
            value = null,
            required = false,
            isMultiple = $(element).attr('multiple');

        //extract the value observable
        if(!ko.isObservable(valueConf) && valueConf && ko.isObservable(valueConf.value)) {
            required = !!valueConf.required;
            value = ko.unwrap(valueConf.value);
        } else if(ko.isObservable(valueConf)) {
            value = ko.unwrap(valueConf);
        }

        if(isMultiple) {
            if (value && value.length > 0) {
                value = ko.mapping.toJS(value);

                // get all keys that are selected
                var keyValues = _.map(value, function(kv) {
                    return kv.keyStr;
                });

                // updates according to keys
                $(element).val(keyValues);
                // refresh boostrap element
                $(element).selectpicker('refresh');
            }
        } else {
            // find the value, and if it's a key/label update the select regarding the key
            if (value && typeof value.key !== 'undefined' && typeof value.label !== 'undefined') {
                value = ko.mapping.toJS(value);

                // update the value. We could check here if the value actually exists in the list of options
                // and decide what to do with key/label that are not in the option list.
                // currently, it displays a blank select if nothing more is done.
                if (value) {
                    $(element).val(value.keyStr);
                } else {
                    $(element).val(null);
                }
            }
        }
    }

    /**
     * error reporting array can either contain the id of the element, or an object containing the id and an error message.
     * extractIds returns an array containing only id's, once, for easy comparison and search
     */
    function extractIds(arrayOfIds) {
        if (!arrayOfIds || arrayOfIds.length === 0) {
            return [];
        }
        return _.uniq(
            _.map(arrayOfIds, function (item) {
                if (item && item.path) {
                    return item.path;
                }
                return item;
            })
        );
    }

    /**
     * if value entered is invalid, report it in the report array
     */
    function reportInvalid(fieldId, report, valueObs) {

        // if the value observable has an isValid sub-observable, set it to false
        if(ko.isObservable(valueObs) && ko.isObservable(valueObs.isValid)) {
            valueObs.isValid(false);
        }

        // setup the error on the form as value is not valid.
        if (report && typeof report === 'function'
            && extractIds(report()).indexOf(fieldId) === -1) {
            report.push(fieldId);
        }
    }

    /**
     * erase error report related to this control if value is valid
     */
    function reportValid(fieldId, report, valueObs) {

        // if the value observable has an isValid sub-observable, set it to true
        if(ko.isObservable(valueObs) && ko.isObservable(valueObs.isValid)) {
            valueObs.isValid(true);
        }

        if (report && typeof report === 'function') {

            var ids = extractIds(report());

            var i = ids.indexOf(fieldId);
            if (i > -1) {
                report.splice(i, 1);
            }
        }
    }

    /**
     * Apply styling to element on init and update for the validationReport binding
     */
    function updateStyle(element, valueAccessor) {
        var idArray = ko.unwrap(valueAccessor());

        // if el.id is in the idArray, make the field red!
        var $el = $(element);
        var $parent = $el.parent();
        var $labelParent = $('label[for="' + $el.attr('id') + '"]').parent();

        // search if id is in the error array
        var ids = extractIds(idArray);

        // if yes, add the has-error class to the parent element of the input,
        // and to the parent element of its label (if not the same)
        if (idArray && $el
            && ids.indexOf($el.attr('id')) > -1) {
            $parent.addClass('has-error');
            if ($labelParent && $labelParent !== $parent) {
                $labelParent.addClass('has-error');
            }
        } else {
            $parent.removeClass('has-error');
            if ($labelParent && $labelParent !== $parent) {
                $labelParent.removeClass('has-error');
            }
        }
    }

    /**
     * called once on binding initialization: setup onblur event on input
     */
    function dateInit(element, valueAccessor, allBindingsAccessor) {
        var valueConf = ko.unwrap(valueAccessor());
        var value = valueConf.value;
        // if a value is required or not (if not provided, assume it's not required)
        var isRequired = valueConf.required;
        // if input accepts years also in addition to full dates
        // if it accepts year, the binding should be an observable where to store the year
        var acceptsYear = valueConf.year;
        // acceptable date range
        var rangeMax = valueConf.max;
        var rangeMin = valueConf.min;

        // <input type=text>
        var $el = $(element);
        // id of input text is used for error reporting
        var fieldId = $el.attr('id');

        // array to report errors to (not necessarily provided)
        var report = allBindingsAccessor().validationReport;

        $el.attr('placeholder', acceptsYear ? config.format.dateAndYearPlaceholder : config.format.datePlaceholder);

        // validate value when focus is lost on input field,
        $el.blur(validateDate);
        // or when 'enter' is pressed inside the input
        $el.keydown(function(event) {
            if(event.which === 13) {
                validateDate();
            }
        });

        /**
         * be sure the date we're dealing with is a valid date.
         * function is called on 'blur' and [ENTER]'s 'keydown' event
         */
        function validateDate () {

            var inputText = $el.val().trim();

            // check required constraint
            if (isRequired && !inputText) {
                reportInvalid(fieldId, report, value);
                return;
            } else if (!inputText) {
                reportValid(fieldId, report, value);
                value(null);
                if (acceptsYear) {
                    acceptsYear(null);
                }
                return;
            }

            // if accepts year also, and current input seems to be a valid 4-digits year,
            // then it is valid. But only the acceptsYear-bound variable will be updated (not the date one
            if (acceptsYear && inputText.match(/^[0-9]{4}$/)) {
                reportValid(fieldId, report, value);
                acceptsYear(+inputText);
                value(null);
                return;
            }

            var date = null;

            if (inputText.match(/^[0-9]{1,2}$/)) {
                // number alone => it's the day of the month
                date = moment(inputText, 'DD');
            } else if (inputText.match(/^[0-9]{1,2}\.[0-9]{1,2}$/)) {
                // dd.mm => it's the day + the month, appent the year
                date = moment(inputText, 'DD.MM');
            } else if (inputText.match(/^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{4}$/)) {
                // check if valid date
                date = moment(inputText, 'DD.MM.YYYY');
            } else if (inputText.match(/^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{2}$/)) {
                // check if valid date DD.MM.YY
                date = moment(inputText, 'DD.MM.YY');
            } else if (inputText.match(/^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{4}\s([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/)) {
                date = moment(inputText, 'DD.MM.YYYY HH:mm');
            }

            var isValid = date && date.isValid();

            if (isValid) {
                reportValid(fieldId, report, value);
                value(date.valueOf());
                if (acceptsYear) {
                    acceptsYear(date.year());
                }
            } else {
                reportInvalid(fieldId, report, value);
            }
        }
    }

    /**
     * Called each time the observable is updated: format input content
     */
    function dateUpdate(element, valueAccessor, allBindingsAccessor) {
        var valueConf = ko.unwrap(valueAccessor());
        var value = ko.unwrap(valueConf.value);
        var acceptsYear = valueConf.year;
        var isDateHM = valueConf.isDateHM;

        if (typeof value !== 'undefined' && value !== null && value !== '') {
            if (!isDateHM) {
                $(element).val(moment(value).format(config.format.dateDefault));
            } else {
                $(element).val(moment(value).format(config.format.dateHM));
            }
        } else if (!acceptsYear) {
            $(element).val('');
        }
    }

    /**
     * init a poursuite nb element
     */
    function poursuiteInit(element, valueAccessor, allBindingsAccessor) {
        var valueConf = ko.unwrap(valueAccessor());
        var value = valueConf.value;
        // if a value is required or not (if not provided, assume it's not required)
        var isRequired = valueConf.required;

        // <input type=text>
        var $el = $(element);
        // id of input text is used for error reporting
        var fieldId = $el.attr('id');

        // array to report errors to (not necessarily provided)
        var report = allBindingsAccessor().validationReport;

        $el.attr('placeholder', config.format.poursuitePlaceholder);

        // validate value when focus is lost on input field
        $el.blur(function () {

            var inputText = $el.val().trim();

            // check required constraint
            if (isRequired && inputText === '') {
                reportInvalid(fieldId, report);
                return;
            } else if (inputText === '') {
                reportValid(fieldId, report);
                value(null);
                return;
            }

            var isValid = noPoursuite.validate(inputText);

            if (isValid) {
                reportValid(fieldId, report);
                value(noPoursuite.normalize(inputText));
            } else {
                reportInvalid(fieldId, report);
            }
        });
    }

    function poursuiteUpdate(element, valueAccessor, allBindingsAccessor) {
        var value = ko.unwrap(valueAccessor().value);

        $(element).val(noPoursuite.reformat(value));
    }

    /**
     * called once on binding initialization: setup onblur event on input
     */
    function genericInit(validationType, element, valueAccessor, allBindingsAccessor) {
        var valueConf = ko.unwrap(valueAccessor());
        var value = valueConf.value;
        // if a value is required or not (if not provided, assume it's not required)
        var isRequired = valueConf.required;

        validationType = validationType || constant.VALIDATE_STRING;

        // <input type=text>
        var $el = $(element);
        if(_validationTypes[validationType].placeholder) {
            $el.attr('placeholder', _validationTypes[validationType].placeholder);
        } 
        
        // id of input text is used for error reporting
        var fieldId = $el.attr('id');

        // array to report errors to (not necessarily provided)
        var report = allBindingsAccessor().validationReport;

        var typeRegex = _validationTypes[validationType].regex;
        var inputToValue = _validationTypes[validationType].inputToValue;
        var rangeMax = valueConf.max;
        var rangeMin = valueConf.min;

        // validate value when focus is lost on input field or when 'enter' is pressed
        $el.blur(validateValue);
        $el.keydown(function(evt) {
            if(evt.which === 13) {
                validateValue();
            }
        });

        function validateValue () {

            var inputText = $el.val().trim();

            if (validationType === constant.VALIDATE_INTEGER) {
                if (rangeMax && rangeMin) {
                    if (inputText > rangeMax || inputText < rangeMin) {
                        reportInvalid(fieldId, report, value);
                        return;
                    }
                }
            }

            // check required constraint
            if (isRequired && inputText === constant.VALUE_BLANK) {
                reportInvalid(fieldId, report, value);
                return;
            } else if (inputText === constant.VALUE_BLANK) {
                reportValid(fieldId, report, value);
                value(null);
                return;
            }

            var isValid = inputText.match(typeRegex);

            if (isValid) {
                reportValid(fieldId, report, value);
                value(inputToValue(inputText));
            } else {
                reportInvalid(fieldId, report, value);
            }
        }
    }

    /**
     * Called each time the observable is updated: format input content
     */
    function genericUpdate(validationType, element, valueAccessor) {
        var valueConf = ko.unwrap(valueAccessor());
        var value = ko.unwrap(valueConf.value);

        validationType = validationType || constant.VALIDATE_STRING;

        var format = _validationTypes[validationType].format;

        if (typeof value !== constant.VALUE_UNDEFINED 
        		&& value !== constant.VALUE_NULL 
        		&& value !== constant.VALUE_BLANK) {
        	
            $(element).val(formatter[format](value));
        } else {
            $(element).val('');
        }
    }
});